fix(driver-memory): an analytics time dimension buckets by its declared granularity (#16178) - #17206
Conversation
…d granularity `AnalyticsQuery.timeDimensions[].granularity` was accepted and never read, so a time dimension answered one group per distinct timestamp — one bar per row in a "new accounts by month" chart, under an ordinary 200 with no warning. The forward bucket labeller is hoisted into `@objectstack/core` as `bucketDateKey`, beside the inverse `bucketKeyToCalendarRange` and the `calendarPartsInTzOrUtc` primitive it already builds on. `@objectstack/objectql`'s `bucketDateValue` becomes a thin delegate with its export name and signature unchanged, so the two in-memory bucketing paths cannot label one instant differently. `driver-memory` folds by granularity between the `$match` half of its pipeline and its `$group`. The bucket key travels under a synthetic field rather than overwriting the row's own, so a member that is both a group key and a measure's aggregand still ranks instants in `max()` while grouping on the label. `second` / `minute` / `hour` are refused at compile with NOT_IMPLEMENTED/501 — the canonical key vocabulary defines no label for a sub-day bucket, and passing one through is the same defect under a new name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…e and its refusal The card's own measurement, with the control that makes it one: two rows on a single UTC day fold to one group under `granularity: 'day'` and stay two groups when nothing asks for a bucket. Beside it: the canonical output vocabulary for all five granularities (the week label is `YYYY-Www`), the reference-timezone fold across three zones, the `dateRange` window left undisturbed on the same entry, the NOT_IMPLEMENTED/501 refusal for the three sub-day intervals asserted on code and status, and the measure-over-the-same-member cell that the synthetic bucket field exists for. `@objectstack/core` gains cells for the labeller itself; `@objectstack/objectql` gains a pin that `bucketDateValue` cannot come apart from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…l's runtime string `check:doc-authoring` reds on an internal issue id inside customer-facing prose: an operator reading a 501 body has no tracker, no git log and no ADR to resolve it against. The anchor stays in the function's doc comment, where the reader who can resolve it is already looking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 34 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c94eb970c6e5462212397959e53894fe8a1db110 && git checkout c94eb970c6e5462212397959e53894fe8a1db110
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4062aef5446b1b07a4016580c3c7680c559538b4 ea3c476badc9ca0f372b25d76d32245b0b93a8a8 && git checkout -B drift-repro 4062aef5446b1b07a4016580c3c7680c559538b4 && git merge --no-ff ea3c476badc9ca0f372b25d76d32245b0b93a8a8
node scripts/docs-audit/affected-docs.mjs --json 4062aef5446b1b07a4016580c3c7680c559538b4
|
Seat rulings — and a correction to my own dispatch orderDelivery accepted. Three rulings, and one admission that belongs first.
|
Contract review at
|
|
Handoff provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
|
Tier notice — the contract-review-tier requirement on this PR is lifted (skills seat, session Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published For this PR: its Generated by Claude Code |
…nsions` never lists
`$group` keyed on `query.dimensions` alone, so the canonical trend shape --
`{measures, timeDimensions: [{dimension, granularity}]}` with no `dimensions`
at all -- was accepted, bucketed nothing and answered ONE TOTAL (`_id: null`).
Accepted, silent and inert is this card's own defect class under a different
name, and the SQL/ObjectQL face already rules the other way: every granular
entry not also listed groups and projects, and `projectedDimensions` hands ONE
set to grouping, row mapping and field metadata alike, because rows carrying a
bucket under a `fields` list that never names it is a trend chart with no
x-axis.
A granular entry now becomes a group key, a projected column and a `fields`
entry, deduped against `dimensions` on the RESOLVED member path so `createdAt`
and `events.createdAt` stay one column. An entry carrying only a `dateRange` is
a predicate and is still not projected -- `timeBuckets` only ever admits an
entry that declared a granularity.
Pinned with its control: the trend shape answers one labelled row and a `fields`
list naming the member; the shape that DOES list the member answers an identical
`fields` list; a `dateRange`-only entry answers one total under a `fields` list
that never mentions it. Measured red before, green after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…ct never declared NOT_IMPLEMENTED/501 is a claim about THIS BACKEND, and its sentence asserts "@objectstack/spec's TimeUpdateInterval declares the value". That is only true of a value the contract actually declares. A caller past the schema door -- `POST /analytics/dataset/query` types `selection.timeDimensions` from `AnalyticsQuery` and never Zod-parses them, the same reachability `analyticsDateRangeUnrecognizedError` records for its own out-of-vocabulary refusal -- can send `fortnight`, and got a 501 telling it something false and pointing it at the backend when the mistake was in the query. The declared vocabulary is now checked first: an undeclared spelling answers `INVALID_QUERY` / 400, and only a declared interval this backend cannot label reaches the 501 arm. Same separation the `dateRange` half of this face already draws. The 400 arm answers the general `StandardErrorCode.INVALID_QUERY` rather than a dedicated `ANALYTICS_GRANULARITY_UNRECOGNIZED` -- the shape its `dateRange` sibling uses -- because a dedicated code has to be registered in `error-code-ledger.zod.ts`, which is a `packages/spec` decision and not a driver patch. Recorded in the doc comment so the choice is visible when that card is written. Pinned with its control through the unparsed door: `fortnight` answers 400, `hour` still answers 501, and the two differ on exactly whether `TimeUpdateInterval` declares the value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
… case, record the new answers Three corrections to text that lands in three CHANGELOGs. The changeset asserted as fact that the `TimeUpdateInterval` narrowing "is filed separately as a `domain:spec` card under ADR-0049". No such card existed when that sentence was written. It does now -- #17296 -- so the sentence names the number instead of repeating a promise. "the window that selects the rows and the bucket that folds them agree on where a calendar day starts" holds for the PRESET arm, which the resolver reads in the reference zone. An explicit `[start, end]` array is the caller's own instant window and keeps its published reading, while the bucket beside it is always a calendar label -- so the two can still disagree. The combination is legitimate and is not refused; the carve-out is stated rather than left to be discovered. And the two answers this round adds are documented where callers read them: a trend query with no `dimensions` now returns labelled rows and a `fields` entry where it returned one bare total, and an undeclared granularity answers 400 rather than a 501 asserting the spec declared it. Grades re-measured, not assumed: no new export in this round -- core stays minor for the three exports it already added, objectql stays minor as the delegate, and driver-memory stays minor with its BREAKING banner under the launch-window lockstep, since the trend-shape change is another instance of the banner's own "an accepted request now answers differently" rather than a new kind of break. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
… has three The breaking-change block opened "BREAKING in two senses, both on driver-memory's analytics face" while the list directly beneath it carries three bullets -- the trend-shape bullet was added in the previous commit and the count words were not. Published text contradicting the list under it, in three CHANGELOGs. Both count words on that line are corrected, since "both" counts the same senses "two" does and would have left the sentence contradicting itself in exactly the same way one word later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
|
| head | packages/runtime content |
Test Core (3/6) |
|---|---|---|
f3961a58f7 |
identical | ✅ success |
ba9e8206f6 |
identical | cancelled — superseded by the next push |
572e172232 |
identical | ❌ failure |
⇒ The same source tree passed this shard and then failed it. 572e172232 differs from ba9e8206f6 by exactly one line of one changeset markdown file (two senses → three senses, both → all). A prose edit in .changeset/ cannot make rollbackToPackageCommit time out. Whatever moved, it was not the diff.
⚠️ ⛔ This seat has no re-run
POST /actions/runs/34439065218/rerun-failed-jobs answers 403 Resource not accessible by integration. The one re-run the rules allow for exactly this case is not available to this identity, so it is recorded as unavailable rather than spent or claimed.
⛔ And the two shortcuts are refused rather than weighed: no raising or removing a timeout on someone else's test to get green, and no empty commit or close-and-reopen to kick CI. Widening this PR into five packages/runtime integration tests it does not otherwise touch would be the widening the same rules forbid.
What was done instead — a real action, ⛔ not a pretext
The PR was 9 commits behind main, measured, so main was merged into the head: new head ea3c476bad, a genuine merge commit with parents 572e172232 and 4062aef544. That is the ordinary action for a PR whose base has moved, it is warranted on its own terms, and it carries a fresh CI run on a current base as a consequence rather than as its purpose.
⭐ ⛔ No history was rewritten: a merge commit, never a rebase, amend or force-push — the implementer's branch stays valid.
Where this stands
CI is now running on ea3c476bad. This PR stays watched with a check-in scheduled until it is green and landed; if the same packages/runtime timeouts recur on a current base, that is a different finding from this one and gets its own card — a repeatedly timing-out integration suite is a real defect in that suite, ⛔ never an "infra flake" to wave through.
The contract-review verdict for this PR is written and gated: the script that posts it reads the head's CI first and refuses to post unless it is converged and green, because NOT MEASURED is never a pass. needs:contract-review therefore stays on both carriers until then.
Generated by Claude Code
Contract review — seat self-review of the PATCH ROUND under the 2026-09-10 maintainer ruling · VERDICT: PASSAuthority: 「你的车道所有任务不需要 fable,任务结束你自己就可以审核」. Routing checked rather than assumed: this PR touches 0 files under Reviewing head 1. F1 — a granular time dimension is a group column. Correct, and the rule was TAKEN rather than invented.
The ⭐ The rule is the SQL/ObjectQL face's own ( 2. F4 — vocabulary first, then capability. Correct, and the ordering is the whole point.
Reachability is not hypothetical: The 3. F3 NOT TAKEN — UPHELD, re-measured by this seat rather than accepted on report.
⇒ A 501 is 4. Published surface and grades — re-measured over the FULL diff, not the round.4 added export lines, all from the original round: Reachability measured rather than assumed: The patch round itself adds no export and touches no barrel, so no grade moves. 5. ⛔ The one thing not waved through — and it was published textThe changeset opened "BREAKING in two senses" while the list beneath it had grown to three bullets. That sentence ships to three CHANGELOGs. Not a nit: #17296 exists because this seat filed a card about a changeset asserting something untrue landing in three published CHANGELOGs, and a standard is worth nothing applied only to other lanes. Fixed in 6. The round's own evidence, checked for shape
7. Docs-drift advisory — checked, nothing owedThe advisory lists 8.
|
Fixes #16178
Clause-②: yesAnalyticsQuery.timeDimensions[].granularitywas accepted bydriver-memory's analytics face and never read, so a time dimension answered one group per distinct timestamp — one bar per row in a "new accounts by month" chart, under an ordinary 200 with no warning anywhere.Reproduction, re-measured on this branch's own base
The card's anchors had moved (PR #17015 rewrote 198 lines of the file), so every anchor here was re-derived by symbol and the card's own controls re-run on
origin/mainfd5cff209:1328902ce)fd5cff209)granularityinmemory-analytics.tsdateRangetimeDimensionsMeasured through the public entry against the built package, two rows on one UTC calendar day (
2026-09-06T01:00:00Z,2026-09-06T23:00:00Z):granularity: 'day'2026-09-06granularity: 'hour'NOT_IMPLEMENTED/ 501The emitted pipeline was byte-identical across all three before the fix. That is the whole finding: the request was accepted, nothing warned, and the key was inert.
The order fenced the change to
packages/drivers/driver-memory/src/memory-analytics.tsplus its tests and a changeset. This PR also editspackages/coreandpackages/objectql. That is not a drift; the fence as written has no executable route inside it.The recorded ruling on the card (director seat, decision batch #91, comment 5583991002) ruled Q1: A and refused the alternatives by name:
objectqlfor the labeller;service-analytics'bucketDate(the non-canonical week label);Inside the fence, the only way to bucket a
weekis to hand-copy the ISO-week rule intodriver-memory, which is exactly the refused D and exactly the divergencecheckDateBucketParityexists to catch. So the fence, taken literally, mandates a ruled-out route. The ruling's own execution note says "one PR spanningcore,objectql,driver-memory", and an earlier claim on the card enumerated that same surface.What the fence's one explicit prohibition asked for is honoured in full:
packages/spec/**is untouched.granularityis already declared there, the spec-side narrowing ofTimeUpdateIntervalis filed separately as issue #17296, adomain:specquestion under ADR-0049, and nothing here crosses that lane.driver-memoryalready declared@objectstack/coreas a runtime dependency, so route A adds no dependency edge.One ruled item is deliberately deferred and not silently dropped: the ruling also asked for ADR-0053's implementation-map row to gain
memory-analytics.tsin this PR.docs/adr/**is a governed surface, and adding it would make this diff governed whole — permanently removing it from the merge queue and reserving the landing to aGOVERNED_APPROVERSreview, a landing-path escalation the dispatch order did not contemplate. It is a one-line documentation row that lands trivially on its own. Say the word and it goes in this PR instead.What it does
@objectstack/core.bucketDateKey(value, granularity, timezone)now sits beside the inversebucketKeyToCalendarRange, theBucketGranularitytype and thecalendarPartsInTzOrUtcprimitive it builds on — all of which were already there.BUCKET_GRANULARITIESandisBucketGranularityname the five granularities that have a canonical key, so a face that must refuse the other three quotes the accepted set instead of hand-listing it. The privateisoWeekLabelUtcand the new labeller now share one statement of the week rule rather than two.@objectstack/objectql'sbucketDateValueis a delegate — export name, signature and answers unchanged. The unreachabledefaultarm's echo is preserved inbucketDateKeyso an off-type JS caller gets the answer it always got.driver-memoryfolds by granularity before its$group. The pipeline is cut at that stage: the$matchhalf still runs in the driver (where the rows and the tenancy guard are), the bucket keys are written onto the selected rows, and the grouping half runs over those. mingo has no expression that produces2026-Q3or2026-W36, and building one out of$isoWeekand$concatwould be the second dialect this repair exists to prevent.created_atin place would leavemax(created_at)ranking bucket labels. Pinned.second/minute/hourare refused at compile withNOT_IMPLEMENTED/ 501 — the classrefusePerAggregationFilteralready uses for the same reason: the query is spelled correctly, the spec declares the value, and it is this backend that compiles nothing for it. Asserted oncodeandstatus, never on message text.How bucketing interacts with the
timezonerepair (#16042)Measured, not assumed. The fold takes
AnalyticsQuery.timezone— the same reference zoneparseDateRangeStringresolves adateRangepreset against — so the window that selects the rows and the bucket that folds them agree on where a calendar day starts. The same two rows:timezone2026-09-06UTC2026-09-06America/New_York2026-09-05,2026-09-06Asia/Tokyo2026-09-06,2026-09-07⇒ UTC is demonstrably not the only case, and the
dateRangewindow is not regressed: both keys on one entry still select by the window's own published semantics and fold what survives. The preset arm and the granularity resolve against onequery.timezone.Coverage
@objectstack/driver-memory@objectstack/core@objectstack/objectqltypecheckclean on all three. All figures taken atf3961a58f7, after theorigin/mainmerge and a full workspace build.Ablation — both legs, proven on disk and through the built artifact
Leg A — restore the defect in the driver. Keying the
$groupon the raw field path again turns 9 of the 14 new cells red. The 5 that stay green are exactly the ones that should: the no-granularity control and the four refusal cells. A blanket failure would have proved much less than this discrimination does.Leg B — mutate the labeller in
core, throughdist. A marker injected intobucketDateKey's week branch,@objectstack/corerebuilt, andablation-dist-preflightconfirming the marker reached 2 built files — thendriver-memorygoes red on exactly its week cell andobjectqlon 3. That is the proof that the driver consumes core's built labeller rather than a stale copy, which no source-only ablation could establish.Both restore legs verified by
git diff HEADempty andgit hash-objectequal to the HEAD blob; leg B's restore additionally re-verified withablation-dist-preflight --absent, whole-tree clean. Neither is a permanent test file.Semver
minoron all three,BREAKINGbanner, ADR-0087not-required (no-migration-prescription).@objectstack/core—minor. Three new public-entry exports. Unambiguous under the house rules.@objectstack/driver-memory—minor. Two events, and neither is apatch. The house rule "repairing an implementation that silently violated its own already-published declared type" would grade the bucketing halfpatch, but that rule does not reach the second half: a value the verb accepted is now refused, which is an accept-set narrowing, not a widening and not an envelope on an existing refusal. And the bucketing half changes the answers an accepted request returns. "Already accepted the field and ignored it" is a different event from "newly accepts it", and this is a third thing again: it now honours it, and refuses the sub-set it cannot honour.minorwith the banner; ⛔majorrefused per the order.@objectstack/objectql—minor. Its own answers do not move by a byte — pinned across granularity, timezone and input form rather than asserted — so on the level axis alone this is apatch. It is gradedminorunder the launch-window lockstep convention its two sibling changesets in this cluster invoke by name ("during the window the bump level is not the carrier, this banner and the disposition above are"), and because the ruling graded all three that way.The card's
priority:p2played no part in any of this; they are unrelated axes.Gates
On head
f3961a58f7(pre-patch-round): 61 families derived byscripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no path argument) and reconciled with--ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN. All 61 exit 0 on that head.ba9e8206f6— see the patch round's own Verification block below for what was re-run locally and what is declared to CI.Two needed a prerequisite before they meant anything and were not reported as passes until they had it:
check:dual-build-cjs-loadsandcheck:type-check-debtboth exited 3 —PREREQUISITE NOT MET— against a partially built tree; a fullpnpm buildwas run and both then exited 0.check-engine-split-ratiorefused on a shallow clone; the clone was deepened as the gate's own remedy line prescribes, and it then measured 98.1%, a repo-wide metric this diff cannot move.One gate found a real defect in the first draft and it is fixed:
check:doc-authoringreds on an internal tracker id inside customer-facing prose, and the refusal message carried one. The anchor moved to the function's doc comment, where a reader who can resolve it is already looking.Acceptance notes
Two boundaries measured on this branch, neither filed, both put in front of the contract review rather than decided here:
The explicit-array
dateRangearm andgranularityanswer in different frames. (Seat ruled A; the carve-out is now stated in the changeset — F5 below.) Withtimezone: 'America/New_York',dateRange: ['2026-09-05', '2026-09-05']andgranularity: 'day', a row at2026-09-05T02:00:00Zis selected by the window (which compares instants, perdriver-memoryanalyticsdateRangeupper bound is INCLUSIVE for a full-timestamp end, so the first instant of tomorrow counts as today #16179's explicit ruling that a caller's window is never narrowed) and then labelled2026-09-04(the NY calendar day, per ADR-0053). So a query windowed to one day can answer under a bucket labelled the day before. Both halves are separately ruled; the seam between them is newly visible because the fold is now timezone-aware. This is a contract question for the Clause-② review, not a bug to fix unilaterally.ARESOLVED in the patch round below (F1). The review ruled this in-fence and pointed at the SQL/ObjectQL face's recorded position (granularityon a member that is not indimensionsis still inert.projectedDimensions, Analytics: a timeDimensions-bucketed query drops the bucket label from rows and fields — trend charts get counts with no x-axis #4033/analytics: 只用来限定「日期区间」的 timeDimension 被补上默认 dateGranularity,于是网格被静默按月拆分 —— 「按 Owner 统计」加个日期筛选就变成「按 Owner × 月」 #5688), which already groups and projects such an entry. It is now implemented and pinned with its control; the note is kept struck rather than deleted so the review's reasoning stays readable against what changed.Patch round — answering contract review
5610525481(REWORK)Head
ba9e8206f6, three commits on top off3961a58f7. Nothing rebased, amended or force-pushed.timeDimensionsentry is now a group key, a projected column and afieldsentry, deduped againstdimensionson the resolved member path.INVALID_QUERY/ 400 instead of a 501 asserting the spec declared it.F1 — the trend shape
$groupkeyed onquery.dimensionsalone, so{measures, timeDimensions: [{dimension, granularity}]}with nodimensionsanswered one total (_id: null). The rule and its exception are taken from the SQL/ObjectQL face rather than invented: every granular entry not also listed groups and projects (objectql-strategy.ts:163-167), one set feeds grouping, row mapping and field metadata alike (projectedDimensions:1889-1893), and adateRange-only entry is a predicate that is not projected (#5688).Pinned with the control the seat named, on the two-rows-one-UTC-day fixture:
dimensions,granularity: 'day'{'events.count': 2}— one total, no time column{'events.createdAt': '2026-09-06', 'events.count': 2}fieldslist['events.count']['events.createdAt', 'events.count'], identical to the shape that DOES list the memberdateRange-only entryfields=['events.count']Measured red before the source change and green after: the two new positive cells failed against the unmodified pre-patch source (
expected { 'events.count': 2 } to match object { …(2) }), and both controls passed before and after, which is what makes them controls.F3 — not taken, and why
The prescribed fix was
err.refusal = true. Its two cited declaration sites arepackages/spec/src/api/contract.zod.tsanderrors.zod.ts(the verdict spells them underdata/), and read there the premise inverts:refusalkey in the ADR-0112 error vocabulary.EnhancedApiErrorSchemadeclarescode,message,userMessage,category,httpStatus,retryable,retryStrategy,retryAfterSeconds,details,fields,timestamp,requestId,traceId,documentation,helpText— and norefusal. Setting one would be metadata the runtime silently discards, which is the class this PR exists to remove.userMessage, and it is declared as the opposite shape.contract.zod.ts:89and its doc block: "Producer-marked user-facing refusal text… A field carrying the text, not a boolean besidemessage… Platform/driver code never sets it."driver-memoryis driver code.classifiedRefusalAnswer(packages/rest/src/error-response.ts:2328) states it in as many words: "What it deliberately refuses to answer: a 5xx, declared or resolved. A server fault is not a refusal addressed to the caller." A declared 5xx never reaches it, by design (/analytics/query仍把 RLS 策略字段名回显给调用方 —— read-scope 拒收的泄漏在姐妹面上没堵,#5367 只堵了 dataset 路由 #5811).So the one-line change would have been inert and undeclared. Recorded here rather than silently skipped; if the seat still wants the guidance to reach the wire, that is the 400-vs-501 question F4 opens, not a boolean.
F4 — out-of-vocabulary is a 400
501 is a claim about this backend, and the refusal's sentence asserted "@objectstack/spec's
TimeUpdateIntervaldeclares the value" — false for'fortnight', which a caller can send past the schema door (POST /analytics/dataset/querytypesselection.timeDimensionsfromAnalyticsQueryand never Zod-parses them, the reachabilityanalyticsDateRangeUnrecognizedErroralready records). The vocabulary is now checked first.StandardErrorCode.INVALID_QUERY, not a dedicatedANALYTICS_GRANULARITY_UNRECOGNIZED— the shape itsdateRangesibling uses. A dedicated code must be registered inpackages/spec'serror-code-ledger.zod.ts, which this round is forbidden to touch and which is a contract decision besides. The choice is recorded in the function's doc comment so it is visible when that card is written; ⛔ nothing published moves either way, because the whole refusal is new in this PR.Semver, re-measured (not assumed)
git diff HEAD -- '*.ts' | grep '^+export 'over the patch round: no new export. No barrel touched.MemoryAnalyticsService.query's signature is unchanged andTimeBucketis a non-exported internal interface. So the grades stand: coreminor(the three exports it already added), objectqlminor(delegate), driver-memoryminor+ banner — F1's trend-shape change is another instance of the banner's existing "an accepted request now answers differently", not a new kind of break.Clause-②: yesalready covers this; nothing here enlarges the published surface beyond what it declares.Verification on
ba9e8206f6pnpm --filter @objectstack/driver-memory test— 49 files, 1200 tests, 0 failures (VERDICT command-exit 0, underscripts/pm/os-verify-lock.sh).pnpm --filter @objectstack/driver-memory typecheck— exit 0. Population measured, not assumed:tsc --listFilesputs the new test file in the program (1 hit of 728 files).pnpm --filter '@objectstack/driver-memory^...' build— exit 0.check:nul-bytes,check:error-code-casing,check:engine-double-contract,check:empty-changeset,check:test-source-alias,check:type-check-coverage,check:published-files— all exit 0.git checkout HEAD -- PATHwithgit diff HEADempty andgit hash-objectmatching the HEAD blob for both files.fieldsloops standing, so 1 of the 4 selected cells reds rather than all four — an honest partial isolation; the whole-change before/after is the red-then-green measurement above.dispatch-gates.mjsderives 61 commands for this change set; the seven above are the ones this diff implicates and the rest are CI's farm.eslintwas narrowed to the three changed source files (--format json: 3 files, 0 errors, 0 warnings) and the narrowing is a measurement, not a guess: this repo's flat config enables no type-aware linting for any file —calculateConfigForFileanswersparserOptions.project: nullandprojectService: null, andeslint.config.mjsrecords the same with a positive control — so this diff cannot move the verdict on any untouched file.dispatch-gates.mjswarns this branch is ≥45 commits behindorigin/mainand that 15 files the derivation reads changed in that range. The list above is therefore a lower bound; re-deriving would need a fresh merge, which this patch round was told not to perform. CI on this head is the authority.Patch round by Claude Code — session
session_01XTBcV7zZHmokdyQgXjbyEU.⛔ Left on this PR as required: the
needs:contract-reviewlabel, draft status, and no ready flip, enqueue, auto-merge or merge.Generated by Claude Code